/* This file is part of BonQ - A simple HTTP-based messaging server.
 * Copyright (C) 2009 James Cook
 *
 * BonQ is free software: you can redistribute it and/or modify it under the
 * terms of the GNU General Public License, version 2, as published by the
 * Free Software Foundation.
 *
 * BonQ is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE.  See the GNU General Public License, version 2,
 * for more details.
 *
 * You should have received a copy of the GNU General Public License, version
 * 2, along with BonQ.  If not, see:
 *
 * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
 */
package com.bonkabonka.queue;

import java.io.InputStreamReader;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;

import javax.servlet.ServletException;
import javax.servlet.ServletConfig;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * This servlet is how one accesses the QueueBucket.  In broad strokes, messages
 * are <code>POST</code>ed to a queue and <code>GET</code>ted [sic] back out
 * (HTTP action verbs are painful).  If no message is available in the queue, an
 * empty (length 0) response is returned.  As a convenience, <code>HEAD</code>
 * can be used to wait for notification of a message arriving in a queue rather
 * than endlessly polling for it.  Optional run-time reconfiguration can happen
 * via the <code>PUT</code> method.
 * 
 * @see #doGet
 * @see #doPost
 * @see #doHead
 * @see #doPut
 * 
 * @author jamesc
 */
public class QueueServlet extends HttpServlet
{
    private static final int BUFFER_SIZE = 8192;

    private QueueBucket queues;

    /**
     * Construct the queue bucket.
     * 
     * @param config ServletConfig provided by the app server
     * @throws javax.servlet.ServletException if the QueueBucket fails to init
     */
    @Override
    public void init(ServletConfig config)
        throws ServletException
    {
        super.init(config);

        String datafile = config.getInitParameter("datafile");
        if(datafile == null)
        {
            throw new ServletException("datafile unspecified");
        }

        try
        {
            queues = new QueueBucket(datafile);
        }
        catch(IOException ioe)
        {
            throw new ServletException(ioe);
        }
    }

    /**
     * Destroy the queue bucket.  An attempt will be made to save the bucket to
     * disk, but there isn't a good recovery plan if that attempt fails.
     *
     * @throws java.lang.RuntimeException if the QueueBucket fails to save
     */
    @Override
    public void destroy()
    {
        try
        {
            queues.save();
        }
        catch(IOException ioe)
        {
            throw new RuntimeException(ioe);
        }

        super.destroy();
    }
    
    /**
     * Provide runtime configuration to the specified queue.  An example of this
     * using cURL:
     * <pre>
     * curl -s -T some.file http://queueserver:2026/queue
     * </pre>
     * The <code>-s</code> option turns off the upload progress indicator while
     * <code>-T</code> indicates what file to upload to the specified URL.  You
     * can use a dash as the filename to indicate that <code>stdin</code> has
     * the data, allowing you to pipe in information from another command.
     * 
     * @param req HttpServletRequest from the app server
     * @param res HttpServletResponse from the app server
     * @throws javax.servlet.ServletException from lookupQueue()
     * @throws java.io.IOException from many things dealing in I/O
     */
    @Override
    public void doPut(HttpServletRequest req, HttpServletResponse res)
        throws ServletException, IOException
    {
        Queue queue = lookupQueue(req.getPathInfo());

        char buffer[] = new char[BUFFER_SIZE];
        StringWriter message = new StringWriter();

        InputStreamReader in = new InputStreamReader(req.getInputStream());
        int count;
        while((count = in.read(buffer)) != -1)
        {
            message.write(buffer, 0, count);
        }
        
        String result;
        synchronized(queue)
        {
            result = queue.configure(message.toString());
        }
        if(result == null)
        {
            result = "";
        }

        res.setContentType("text/plain");
        res.setContentLength(result.length());

        PrintWriter out = res.getWriter();
        out.print(result);
        out.flush();
        out.close();

        res.flushBuffer();
    }

    /**
     * Puts a message into the specified queue.  An example using cURL:
     * <pre>
     * curl --data "message" http://queueserver:2026/queue
     * </pre>
     * NOTE: if you don't want cURL to munge newlines and other whitespace use
     * <code>--data-binary</code> instead.
     * 
     * @param req HttpServletRequest from the app server
     * @param res HttpServletResponse from the app server
     * @throws javax.servlet.ServletException from lookupQueue()
     * @throws java.io.IOException from many things dealing in I/O
     */
    @Override
    public void doPost(HttpServletRequest req, HttpServletResponse res)
        throws ServletException, IOException
    {
        Queue queue = lookupQueue(req.getPathInfo());

        char buffer[] = new char[BUFFER_SIZE];
        StringWriter message = new StringWriter();

        InputStreamReader in = new InputStreamReader(req.getInputStream());
        int count;
        while((count = in.read(buffer)) != -1)
        {
            message.write(buffer, 0, count);
        }

        String result;
        synchronized(queue)
        {
            if(queue instanceof QueriedQueue)
            {
                result = ((QueriedQueue)queue).push(message.toString(),
                                                    req.getQueryString());
            }
            else
            {
                result = queue.push(message.toString());
            }
            queue.notifyAll();
        }

        if(result == null)
        {
            result = "";
        }

        res.setContentType("text/plain");
        res.setContentLength(result.length());

        PrintWriter out = res.getWriter();
        out.print(result);
        out.flush();
        out.close();

        res.flushBuffer();
    }

    /**
     * Waits for a message to arrive in the specified queue.  Note that the
     * message is <strong>NOT</strong> returned by this method, rather only
     * an indication whether or not a message has arrived.  This is due to the
     * unreliable nature of HTTP as it traverses poorly-configured firewalls,
     * routers and other bits of network equipment hostile to long-lived
     * connections.  An example using cURL to wait forever for a message:
     * <pre>
     * curl -I http://queueserver:2026/queue
     * </pre>
     * If you would rather limit the amount of time to wait, you can specify a
     * timeout (in seconds) in the query string.  Thus, to have the above sample
     * wait a maximum of ten seconds, the following command would be used:
     * <pre>
     * curl -I http://queueserver:2026/queue?10
     * </pre>
     * A status code of OK (200) means that a message is waiting.  A NO_CONTENT
     * (204) means, unsurprisingly, that there is not a message waiting.  Also,
     * the length can be used to determine whether a message waits or not - a
     * non-zero length means there is a message, zero means there is not.
     * 
     * @param req HttpServletRequest from the app server
     * @param res HttpServletResponse from the app server
     * @throws javax.servlet.ServletException from lookupQueue()
     * @throws java.io.IOException from many things dealing in I/O
     */
    @Override
    public void doHead(HttpServletRequest req, HttpServletResponse res)
            throws ServletException, IOException
    {
        Queue queue = lookupQueue(req.getPathInfo());
        
        long timeout = 0;
        if(req.getQueryString() != null)
        {
            try
            {
                timeout = Long.parseLong(req.getQueryString()) * 1000;
                if(timeout < 0)
                {
                    throw new NumberFormatException("negative wait period");
                }
            }
            catch(NumberFormatException nfe)
            {
                res.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
                              nfe.toString());
                return;
            }
        }

        synchronized(queue)
        {
            try
            {
                if(queue.peek() == null)
                {
                    queue.wait(timeout);
                }
            }
            catch(InterruptedException ie)
            {
                res.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
                              ie.toString());
                return;
            }
            
            int status = HttpServletResponse.SC_NO_CONTENT;
            int length = 0;

            String message = queue.peek();
            if(message != null)
            {
                status = HttpServletResponse.SC_OK;
                length = message.length();
            }
            
            res.setContentType("text/plain");
            res.setContentLength(length);
            res.setStatus(status);

            res.flushBuffer();
        }
    }
    
    /**
     * Fetches a message from the specified queue.  If a message is not waiting
     * then an empty (zero-length) response is returned.  An example using cURL:
     * <pre>
     * curl http://queueserver:2026/queue
     * </pre>
     * If you do not wish to remove the message from the queue, then append
     * <code>peek</code> as the query string to the URL.  Using cURL this would
     * look like:
     * <pre>
     * curl http://queueserver:2026/queue?peek
     * </pre>
     * 
     * @param req HttpServletRequest from the app server
     * @param res HttpServletResponse from the app server
     * @throws javax.servlet.ServletException from lookupQueue()
     * @throws java.io.IOException from many things dealing in I/O
     */
    @Override
    public void doGet(HttpServletRequest req, HttpServletResponse res)
            throws ServletException, IOException
    {
        Queue queue = lookupQueue(req.getPathInfo());

        synchronized(queue)
        {
            String message = queue.peek();
            if(message == null)
            {
                message = "";
            }

            res.setContentType("text/plain");
            res.setContentLength(message.length());

            PrintWriter out = res.getWriter();
            out.print(message);
            out.flush();
            out.close();

            res.flushBuffer();

            // In theory, an IOException should occur prior to this point if
            // the client aborted the connection.  Too bad it doesn't actually
            // seem to work that way.
            if(!"peek".equals(req.getQueryString()))
            {
                queue.pull();
            }
        }
    }
    
    /**
     * Take the provided path, fetch the named queue.
     * 
     * @param path String containing the servlet's pathInfo
     * @return Queue if found
     * @throws javax.servlet.ServletException if the name is invalid/not found
     */
    private Queue lookupQueue(String path)
            throws ServletException
    {
        if(path == null || !path.startsWith("/"))
        {
            throw new ServletException("Invalid queue name");
        }
        
        // Lop off leading slash
        path = path.substring(1);
        
        Queue queue = queues.get(path);
        if(queue == null)
        {
            throw new ServletException("Invalid queue name '" + path + "'");
        }
        
        return queue;
    }
}
